Skip to content

feat: social account linking#515

Open
luciorubeens wants to merge 7 commits intoallinbits:mainfrom
luciorubeens:feat/social-network-name
Open

feat: social account linking#515
luciorubeens wants to merge 7 commits intoallinbits:mainfrom
luciorubeens:feat/social-network-name

Conversation

@luciorubeens
Copy link
Copy Markdown
Member

@luciorubeens luciorubeens commented Mar 2, 2026

Summary

  • Added social account linking so users can map their external username/handle to their address.
  • It works through a proof verification similar to keybase.io: users post a message containing their address on the social platform, then submit the proof URL. The backend checks if the proof is valid and updates the status.
  • For now it is integrated with X (Twitter) and GitHub. More providers can be added later.
  • Updated the backend API to return the associated social handles together with feed, replies, and notifications data when available.

Fixes #508

Dither.Demo.mp4

Screenshots

SCR-20260318-rdwe
Failed verification with retry option
SCR-20260318-qhrb
Profile with multiple verified social handles

@jeronimoalbi
Copy link
Copy Markdown
Member

Great work 👍

Something we should consider is to use names like lucio@x or lucio@github instead. Eventually we might want to integrate with Gno.land names registry for example, when the feature is available, and those might eventually be the ones without suffix.

@luciorubeens
Copy link
Copy Markdown
Member Author

@jeronimoalbi Yes, we're already storing it that way. The only difference is that the Username component will render it as a styled UI badge.

To tag someone, you’ll need to use the user@platform format.

Btw we also need to update the /profile endpoint to support username resolution.

@luciorubeens
Copy link
Copy Markdown
Member Author

The PR is ready for review. Supporting tagging other users while posting will be the next step.

One thing to note before deploying: we’ll propably need to introduce migrations using Drizzle (https://orm.drizzle.team/docs/migrations). Currently, we're just pushing the schema directly to the database (https://github.com/allinbits/dither.chat/blob/main/packages/api-main/package.json#L17).

* matches the code verified against the proof tweet.
*/
export function getSocialProofCode(address: string): string {
const withoutPrefix = address.replace(/^[a-z]+1/, '');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using a regular expression instead of slice(5)?

export async function LinkSocial(action: ActionWithData): Promise<ResponseStatus> {
try {
const [username, platform, proofUrl] = extractMemoContent(action.memo, 'dither.LinkSocial');
const postBody: { hash: string; from: string; username: string; platform: string; proof_url: string; timestamp: string } = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const postBody: { hash: string; from: string; username: string; platform: string; proof_url: string; timestamp: string } = {
const postBody: Posts.SocialProofBody = {

Comment on lines +1 to +2
/* eslint-disable ts/no-namespace */
import type { ActionWithData, ResponseStatus } from '../types/index';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing it allows to use Posts.SocialProofBody in the next comment:

Suggested change
/* eslint-disable ts/no-namespace */
import type { ActionWithData, ResponseStatus } from '../types/index';
/* eslint-disable ts/no-namespace */
import type { Posts } from '@atomone/dither-api-types';
import type { ActionWithData, ResponseStatus } from '../types/index';

The space in between the imports is for consistency with all of the other files in the same folder than this one.

Comment on lines +62 to +64
## Social Verification

Social username verification is asynchronous:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Social Verification
Social username verification is asynchronous:
## Social Verification
Verification is done to link an AtomOne address to one or more social account usernames.
Social username verification is asynchronous:

hash: action.hash,
from: action.sender,
username,
platform,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate that platform is valid before anything else I think, if it fails we could send an internal message to the user.

Comment on lines +27 to +29
if (tokenAddress && tokenAddress.toLowerCase() === address) {
isOwner = true;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe?

Suggested change
if (tokenAddress && tokenAddress.toLowerCase() === address) {
isOwner = true;
}
isOwner = (tokenAddress && tokenAddress.toLowerCase() === address)

// Verification runs in background, we don't want to block the API response.
// Future improvement: if we had a job queue system, we could push a job here instead of this.
verifyLink(insertedId, body.platform.toLowerCase(), body.proof_url, body.from.toLowerCase(), body.username.toLowerCase()).catch(
err => console.error(`verifySocialLink error for id=${insertedId}:`, err),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a good idea to send a notification to the user with the failure reason, otherwise it might happen that verification fails and user ends up only seeing the social link pending for a long time.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth considering maybe using a scheduled GitHub CI workflow that runs from time to time and tries to verify accounts created a while ago that failed to be verified within the max retries threshold.

Comment on lines +11 to +12
const MAX_RETRIES = 3;
const INITIAL_DELAY_MS = 2000; // 2 seconds, doubles each retry
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about increasing max retries to 7 or 10 retries?

Also maybe even increasing the initial delay a bit. Increasing both has the potential to work if there are long hickups with a service.

* Never throws — all errors are caught and result in a 'failed' DB update.
*/
export async function verifyLink(
id: number,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change id to socialLinkId for clarity?

Comment on lines +33 to +39
if (await isHandleAlreadyClaimed(username, platform, id)) {
await getDatabase()
.update(SocialLinksTable)
.set({ status: STATUS_FAILED, error_reason: ERROR_REASON_HANDLE_ALREADY_CLAIMED })
.where(eq(SocialLinksTable.id, id));
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should provably be the first thing to do for both cases when this function is called to make sure handle is not claimed before doing anything else. It would remove the duplicated code too.

eq(SocialLinksTable.handle, handle),
eq(SocialLinksTable.platform, platform),
eq(SocialLinksTable.status, STATUS_VERIFIED),
ne(SocialLinksTable.id, excludeId),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the excludeId really needed? If the handle is already claimed it wouldn't matter who did it, isn't it?

@jeronimoalbi
Copy link
Copy Markdown
Member

jeronimoalbi commented Mar 30, 2026

The PR is ready for review. Supporting tagging other users while posting will be the next step.

One thing to note before deploying: we’ll propably need to introduce migrations using Drizzle (https://orm.drizzle.team/docs/migrations). Currently, we're just pushing the schema directly to the database (https://github.com/allinbits/dither.chat/blob/main/packages/api-main/package.json#L17).

It's definitely needed 👍

return providers.find(p => p.id === platform.value) ?? null;
});

const display = computed(() => handle.value ? `@${handle.value}` : shorten(props.userAddress || '...............', 8, 4));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "@" prefix could be removed, otherwise username would look like @elon@x:

Suggested change
const display = computed(() => handle.value ? `@${handle.value}` : shorten(props.userAddress || '...............', 8, 4));
const display = computed(() => handle.value ? `${handle.value}` : shorten(props.userAddress || '...............', 8, 4));


<Tabs v-if="wallet.loggedIn.value" :tabs="tabs" layout="fill" class="border-t" />
<!-- Social Accounts -->
<SocialAccountsPanel
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about moving the verification forms to settings? It seems a bit off in the user profile page.

Profile could display the already verified accounts only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready to Build

Development

Successfully merging this pull request may close these issues.

Support for mapping social names to an address

2 participants